import { normalizePath } from '@rollup/pluginutils'
import chalk from 'chalk'
import { existsSync, mkdirs } from 'fs-extra'
import { readFile } from 'fs/promises'
import { once } from 'lodash-es'
import path from 'path'
import { PluginHooks } from 'rollup'
import * as pkg from '../../package.json'
import { COMMONJS_REQUIRE } from '../constants'
import { ResolvedOptions } from '../options'
import { addExtension, getSourcePath, removeExtension, safeRequire } from '../utils'
import { getHelper as getBaseHelp } from './base'
import asLoaderHelper from './modules/wasm-asc'
import { getHelper as getWebAssemblyHelper } from './webAssembly'
import { defineHelper } from './_utils'

const _getCompiler = async (resolvedAscOptions: ResolvedOptions['asc']) => {
  const { binaryDir, textDir, jsDir, idlDir, tsdDir } = resolvedAscOptions

  try {
    var asc = await import('assemblyscript/cli/asc')
    console.info(
      chalk.yellow`[${pkg.name}]: Found dependency "assemblyscript". Version is ${
        asc.version ?? safeRequire('assemblyscript/package.json')?.version
      }.`
    )
  } catch (err) {
    console.error(
      chalk.red`[${pkg.name}]: Dependency "assemblyscript" not found. Did you install it?`
    )
    return
  }
  try {
    await Promise.all([
      mkdirs(binaryDir),
      textDir && mkdirs(textDir),
      jsDir && mkdirs(jsDir),
      idlDir && mkdirs(idlDir),
      tsdDir && mkdirs(tsdDir),
      asc.ready,
    ])

    const compile = (input: string, options?: import('assemblyscript/cli/asc').APIOptions) => {
      const name = normalizePath(resolvedAscOptions.rename(input))
      const noExtName = removeExtension(name, '.wasm')
      const binFile = path.join(binaryDir, name)
      const sourceMapFile = addExtension(binFile, '.map')
      const textFile = textDir != null && path.join(textDir, addExtension(noExtName, '.wat'))
      const jsFile = jsDir != null && path.join(jsDir, addExtension(noExtName, '.js'))
      const idlFile = idlDir != null && path.join(idlDir, addExtension(noExtName, '.webidl'))
      const tsdFile = tsdDir != null && path.join(tsdDir, addExtension(noExtName, '.d.ts'))

      return new Promise<{
        compiledFiles?: string[]
        binFile: string
        sourceMapFile: false | string
        textFile: false | string
        jsFile: false | string
        idlFile: false | string
        tsdFile: false | string
        name: string
      }>((resolve, reject) => {
        const argv = [
          input,
          `--binaryFile=${binFile}`,
          textFile && `--textFile=${textFile}`,
          jsFile && `--jsFile=${jsFile}`,
          idlFile && `--idlFile=${idlFile}`,
          tsdFile && `--tsdFile=${tsdFile}`,
          `--sourceMap=${sourceMapFile}`,
          ...resolvedAscOptions.compilerArgv,
        ].filter(Boolean) as string[]

        asc.main(
          argv,
          {
            ...options,
          },
          (err) => {
            if (err) {
              reject(err)
              return 1
            }

            readFile(sourceMapFile)
              .catch(() => undefined)
              .then(async (sourceMapSources) => {
                let compiledFiles: string[] | undefined
                if (sourceMapFile && sourceMapSources) {
                  try {
                    const sourceMapSourcesStr = sourceMapSources
                      .toString()
                      .match(
                        /(?<="sources"\s*:\s*)\[\s*(?:(?:"[^"\r\n]*")(?:\s*,\s*"[^"\r\n]*")*)?\s*\]/
                      )?.[0]
                    if (sourceMapSourcesStr) {
                      const sourceMapSources = JSON.parse(sourceMapSourcesStr)
                      if (Array.isArray(sourceMapSources)) {
                        compiledFiles = sourceMapSources
                          .map((file) => {
                            if (file.startsWith('~')) return
                            if (existsSync(file)) {
                              return path.resolve(file)
                            }
                          })
                          .filter(Boolean) as string[]
                      }
                    }
                  } catch (error) {
                    console.error(chalk.red(error))
                  }
                }

                resolve({
                  compiledFiles,
                  binFile,
                  sourceMapFile,
                  textFile,
                  jsFile,
                  idlFile,
                  tsdFile,
                  name,
                })
              })
              .catch(reject)

            return 0
          }
        )
      })
    }

    return compile
  } catch (e) {
    console.error(chalk.red(e))
  }
}

export const getHelper = defineHelper((options, registerVirtualModule) => {
  const _getAssemblyScriptCompiler = once(() => _getCompiler(options.asc))
  const wasmHelper = getWebAssemblyHelper(options)
  const utilsHelper = getBaseHelp(options)

  const ASC_INTERNAL_PREFIX = 'WASM-ASC:'
  const ASC_INTERNAL_SUFFIX = '?asc'
  const ASC_MODULE_RE = /^(?:asc|assemblyScript):(.+)$/

  const resolveId: PluginHooks['resolveId'] = async function (id, importer, options) {
    let match

    if (id.endsWith(COMMONJS_REQUIRE)) {
      id = id.slice(0, id.length - COMMONJS_REQUIRE.length)
    }

    if ((match = id.match(ASC_MODULE_RE))) {
      const result = await getSourcePath(this, match[1], importer, ['.as', '.ts'], options)
      if (result) return ASC_INTERNAL_PREFIX + result + ASC_INTERNAL_SUFFIX
    } else if (id.startsWith(ASC_INTERNAL_PREFIX) && id.endsWith(ASC_INTERNAL_SUFFIX)) {
      return id
    }
  }

  const load: PluginHooks['load'] = async function (id) {
    const rawId = id
    if (id.startsWith(ASC_INTERNAL_PREFIX) && id.endsWith(ASC_INTERNAL_SUFFIX)) {
      id = id.slice(ASC_INTERNAL_PREFIX.length)
      id = id.slice(0, id.length - ASC_INTERNAL_SUFFIX.length)

      const compile = await _getAssemblyScriptCompiler()
      if (!compile) return

      const { compiledFiles, binFile } = await compile(id)

      if (compiledFiles) {
        utilsHelper.addWatchFile(this, rawId, ...compiledFiles)
      }
      return await wasmHelper._generateLoaderCode(this, id, binFile, asLoaderHelper)
    } else if (options.asc.filter(id)) {
      const stringifyId = JSON.stringify(ASC_INTERNAL_PREFIX + id + ASC_INTERNAL_SUFFIX)
      return `export * from ${stringifyId};export {default} from ${stringifyId};`
    }
  }

  return { resolveId, load }
})
